package com.videri.match.server;

import com.db4o.ObjectContainer;
import com.db4o.query.Predicate;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.videri.match.client.Matching;
import com.videri.match.client.Category;
import com.videri.match.client.VideriMatchService;
import java.util.*;
import java.io.*;
import com.db4o.*;
import com.db4o.query.*;
import javax.servlet.*;
import java.lang.Thread;


/**
 * This Servlet is the back end of the VideriMatch
 * webpage.  It is responsible for handling all information
 * for all games.  It can store information about newly
 * created games as well as passing information about 
 * how to set up games.  It also maintains a means of persistence.
 * @author Michael Bauer
 *
 */
public class VideriServlet extends RemoteServiceServlet implements
		VideriMatchService {
	
	private static final long serialVersionUID = -3051849539027056063L;
	//This is used to control the maximum size of the most recent and most popular categories
	private final int CATEGORY_SIZE = 100;
	
	// CHANGE THIS IN ORDER FOR THE SERVER TO BE PERSISTENT ON YOUR MACHINE!!!
	private final String SERVER_STATE_DIR = "C:\\Documents and Settings\\Michael Bauer\\workspace\\tmp\\";
	
	//This will control the size of the cookies to be used
	private final int COOKIE_SIZE=30;
	
	// This is the string associated with the cookieMapping attribute
	private final String COOKIE_ATTRIBUTE = "Servlet Cookies";
	
	// This is the string associated with the waiting player number attribute
	private final String WAITING_PLAYER_NUM = "Head to head waiting player";
	
	/*
	private Map myMatchings;
	private Map myCategories;*/
	private Map myAccessCount;
	private Set myMostPopular;
	private TreeMap myMostRecent;
	private Map myLoggedInUsers;
	private Map myHead2HeadGames;
	private Head2HeadGame myRecentGame;
	private int waitingPlayerNum;
	
	ObjectContainer matchingsDB;
	ObjectContainer categoriesDB;
	ObjectContainer accessCountDB;
	ObjectContainer mostPopularDB;
	ObjectContainer mostRecentDB;
	ObjectContainer userDB;
	
	private Random myRandomGenerator;
 
	public boolean addNewMatching(final Matching mat, final String category) {
		if(matchingsDB==null)
			initServerState();
		List matchings = matchingsDB.query(new Predicate() {
			public boolean match(Matching m){
				return mat.equals(m.getName());
			}
		});
		if(matchings.size()==0){
			matchingsDB.set(mat);
			List categories = categoriesDB.query(new Predicate(){
				public boolean match(Category c){
					return category.equals(c.getCategoryName());
				}
			});
			Category cat;
			if(categories.size()==0)
				cat = new Category(category);
			else
				cat = (Category)categories.get(0);
			cat.addMatching(mat);
			categoriesDB.set(cat);
			myAccessCount.put(mat.getName(),new Integer(0));
			updateAccessCount();
			updateMostRecent(mat);
		}
		return false;
		/*
		if(!(myMatchings.keySet().contains(match.getName()))){
			myMatchings.put(match.getName(), match);
			Category cat;
			if(myCategories.containsKey(category))
				cat = (Category)myCategories.get(category);
			else
				cat = new Category(category);
			cat.addMatching(match);
			myCategories.put(category, cat);
			myAccessCount.put(match.getName(),new Integer(0));
			updateMostRecent(match);
			makePersistent();
			return true;
		}
		return false;*/
	}

	public Matching getMatching() {
		if(matchingsDB==null)
			initServerState();
		List matches = matchingsDB.query(new Predicate(){
			public boolean match(Matching m){
				return true;
			}
		});
		int loc = myRandomGenerator.nextInt(matches.size());
		Matching m = (Matching)matches.get(loc);
		return m;		
	}

	public Matching getMatching(final String name, boolean fromCategory) {
		if(matchingsDB==null)
			initServerState();
		Matching match;
		if(fromCategory){
			List cat = categoriesDB.query(new Predicate() {
				public boolean match(Category c){
					return name.equals(c.getCategoryName());
				}
			});
			if(cat.size()>0){
				final Set matchingNames = ((Category)cat.get(0)).getMatchingNames();
				List matches = matchingsDB.query(new Predicate() {
					public boolean match(Matching m){
						return matchingNames.contains(m.getName());
					}
				});
				int loc = myRandomGenerator.nextInt(matches.size());
				Iterator it = matches.iterator();
				while(it.hasNext() && loc>0){
					it.next();
					loc--;
				}
				match = (Matching)it.next();
			}
			else{
				match = null;
				System.err.println("Warning: Unable to locate category!");
			}
		}
		else{
			List matches = matchingsDB.query(new Predicate() {
				public boolean match(Matching m){
					return name.equals(m.getName());
				}
			});
			if(matches.size()>0)
				match=(Matching)matches.get(0);
			else{
				match = null;
				System.err.println("Warning: Unable to locate matching!");
			}
		}
		myAccessCount.put(match.getName(), new Integer(((Integer)myAccessCount.get(match.getName())).intValue()+1));
		updateAccessCount();
		updateMostPopular(match);
		return match;
	}
	
	public Category getCategory(final String name, int numMatchings){
		if(matchingsDB==null)
			initServerState();
		Category ret = new Category(name);
		List cat = categoriesDB.query(new Predicate() {
			public boolean match(Category c){
				return name.equals(c.getCategoryName());
			}
		});
		if(cat.size()>0 || name.equals("Most Popular")){
			Set names;
			if(name.equals("Most Popular"))
				names = myMostPopular;
			else{
				if(cat.size()>0)
					names = ((Category)cat.get(0)).getMatchingNames();
				else{
					names = null;
					System.err.println("Warning: Category unable to be found!");
				}
			}
			HashSet topNames = new HashSet();
			if(names.size()>numMatchings){
				while(topNames.size()<numMatchings){
					Iterator it = names.iterator();
					int max=-1;
					String next=new String();
					while(it.hasNext()){
						String temp = (String)it.next();
						if(topNames.contains(temp))
							continue;
						if(((Integer)myAccessCount.get(temp)).intValue()>max){
							max=((Integer)myAccessCount.get(temp)).intValue();
							next=temp;
						}
					}
					topNames.add(next);
				}
			}
			else
				topNames.addAll(names);
			Iterator it = topNames.iterator();
			while(it.hasNext())
			{
				final String s = (String) it.next();
				List matches = matchingsDB.query(new Predicate() {
					public boolean match(Matching m){
						return s.equals(m.getName());
					}
				});
				if(matches.size()>0)
					ret.addMatching((Matching)matches.get(0));
				else
					System.err.println("Warning: Matching unable to be found!");
			}
		}
		return ret;
	}
	
	public Category[] getAllCategories(int numMatchings){
		if(matchingsDB==null)
			initServerState();
		List cats = categoriesDB.query(new Predicate() {
			public boolean match(Category c){
				return true;
			}
		});
		Category[] ret = new Category[cats.size()+2];
		Iterator it = cats.iterator();
		for(int i=0; i<cats.size(); i++){
			String name = ((Category)it.next()).getCategoryName();
			ret[i] = getCategory(name, numMatchings);
		}
		// Add Most Recent as a Category
		Category mostRecent = new Category("Most Recent");
		Iterator it2 = myMostRecent.keySet().iterator();
		int numLeft = myMostRecent.size();
		while(it2.hasNext()){
			if(numLeft<=numMatchings){
				final String name = (String)myMostRecent.get(it2.next());
				List matches = matchingsDB.query(new Predicate() {
					public boolean match(Matching m){
						return name.equals(m.getName());
					}
				});
				if(matches.size()>0)
					mostRecent.addMatching((Matching)matches.get(0));
				else
					System.err.println("Warning: Matching unable to be found!");
			}
			else{
				it2.next();
				numLeft--;
			}
		}
		ret[cats.size()] = mostRecent;
		
		// Add Most Popular as a Category
		ret[cats.size()+1] = getCategory("Most Popular", numMatchings);
		
		return ret;
	}

	public boolean createNewUser(String[] userInfo) {
		if(matchingsDB==null)
			initServerState();
		final String[] props = userInfo;
		// This queries to check to see if any other users already have overlapping properties
		List users = userDB.query(new Predicate() {
			public boolean match(VideriUser v){
				if(props[0].equals(v.getHandle()) || (props[1].equals(v.getLastName()) && props[2].equals(v.getFirstName())) || props[4].equals(v.getEmail()))
					return true;
				return false;
			}
		});
		if(users.size()==0){
			VideriUser v = new VideriUser(userInfo[0], userInfo[1], userInfo[2], userInfo[3], userInfo[4]);
			userDB.set(v);
			return true;
		}
		return false;
	}

	public String[] getUserProperties(final String handle) {
		if(matchingsDB==null)
			initServerState();
		String[] properties = new String[4];
		List users = userDB.query(new Predicate() {
			public boolean match(VideriUser v){
				return handle.equals(v.getHandle());
			}
		});
		if(users.size()>0){
			VideriUser u = (VideriUser)users.get(0);
			properties[0]=(new Integer(u.getRating())).toString();
			properties[1]=(new Double(u.getPercentile())).toString();
			properties[2]=(new Integer(u.getNumSolvedPuzzles())).toString();
			properties[3]=(new Integer(u.getNumTriedPuzzles())).toString();
		}
		return properties;
	}

	public String loginUser(final String handle, final String password) {
		if(matchingsDB==null)
			initServerState();
		List user = userDB.query(new Predicate() {
			public boolean match(VideriUser v){
				if(handle.equals(v.getHandle()) && password.equals(v.getPassword()))
					return true;
				return false;
			}
		});
		String cookie = "";
		if(user.size()==1){
			for(int i=0; i<COOKIE_SIZE; i++)
				cookie += (char)(myRandomGenerator.nextInt(116)+10);
			myLoggedInUsers.put(cookie,user.get(0));
		}
		return cookie;
	}

	public boolean logoutUser(final String cookie) {
		if(matchingsDB==null)
			initServerState();
		return myLoggedInUsers.remove(cookie)!=null;
	}

	public boolean matchingResult(String gameHandle, boolean finished,
			double score) {
		//TODO: implement this method
		if(matchingsDB==null)
			initServerState();
		return false;
	}
	
	public int startNewHead2HeadGame(int playerNum){
		if(matchingsDB==null)
			initServerState();
		if(myRecentGame!=null && myRecentGame.getOtherPlayer(playerNum)!=-1){
			return myRecentGame.getGameNum();
		}
		else{
			ServletContext con = this.getServletContext();
			Integer otherPlayer = (Integer)con.getAttribute(WAITING_PLAYER_NUM);
			if(otherPlayer.intValue()!=-1 && otherPlayer.intValue()!=playerNum){
				System.out.println("Starting new game...");
				int gameNum = myRandomGenerator.nextInt(1000000);
				myRecentGame = new Head2HeadGame(gameNum, playerNum, otherPlayer.intValue(), getMatching());
				myHead2HeadGames.put(new Integer(gameNum), new Head2HeadGame(gameNum, playerNum, otherPlayer.intValue(), myRecentGame.getMatching()));
				return gameNum;
			}
		}
		return -1;
	}
	
	public Matching getHead2HeadMatching(int gameNum){
		if(myHead2HeadGames.containsKey(new Integer(gameNum))){
			Head2HeadGame h = (Head2HeadGame)myHead2HeadGames.get(new Integer(gameNum));
			return h.getMatching();
		}
		return null;
	}
	
	public boolean checkHead2HeadGameWon(int gameNum){
		Integer num = new Integer(gameNum);
		if(myHead2HeadGames.containsKey(num)){
			myHead2HeadGames.remove(num);
			return false;
		}
		return true;
	}
	
	public void destroy(){
		closeDatabases();
		super.destroy();
	}
	
	private void closeDatabases(){
		matchingsDB.close();
		categoriesDB.close();
		accessCountDB.close();
		mostRecentDB.close();
		mostPopularDB.close();
		userDB.close();
	}
	
	private void updateMostRecent(Matching match){
		if(myMostRecent.size()==CATEGORY_SIZE)
			myMostRecent.remove(myMostRecent.firstKey());
		myMostRecent.put(match.getCreationTime(), match.getName());
		
		List a = mostRecentDB.query(new Predicate() {
			public boolean match(Map m){
				return true;
			}
		});
		
		if(a.size()>0){
			mostRecentDB.delete(a.get(0));
		}
		mostRecentDB.set(myMostRecent);
	}
	
	private void updateAccessCount(){
		List a = accessCountDB.query(new Predicate() {
			public boolean match(Map m){
				return true;
			}
		});
		if(a.size()>0){
			accessCountDB.delete(a.get(0));
		}
		accessCountDB.set(myAccessCount);
	}
	
	private void updateMostPopular(Matching match){
		if(myMostPopular.size()==CATEGORY_SIZE){
			Iterator it=myMostPopular.iterator();
			String name = null;
			int smallest = ((Integer)myAccessCount.get(match.getName())).intValue();
			while(it.hasNext()){
				String temp = (String)it.next();
				int val = ((Integer)myAccessCount.get(temp)).intValue();
				if(val<smallest){
					smallest = val;
					name = temp;
				}
			}
			if(name!=null){
				myMostPopular.remove(name);
				myMostPopular.add(match.getName());
			}
		}
		else{
			myMostPopular.add(match.getName());
		}
		
		List a = mostPopularDB.query(new Predicate() {
			public boolean match(Set t){
				return true;
			}
		});
		
		if(a.size()>0){
			mostPopularDB.delete(a.get(0));
		}
		mostPopularDB.set(myMostPopular);
	}
	
	//This will initialize the server state if it hasn't already been done
	private void initServerState(){
		myRandomGenerator = new Random();
		
		/*try{
			FileInputStream fis = new FileInputStream(SERVER_STATE_FILE);
			ObjectInputStream ois = new ObjectInputStream(fis);
			myMatchings = (Map)ois.readObject();
			myCategories = (Map)ois.readObject();
			myAccessCount = (Map)ois.readObject();
			myMostPopular = (Set)ois.readObject();
			myMostRecent = (TreeMap)ois.readObject();
			ois.close();
		}
		catch(FileNotFoundException e){
			System.err.println("Warning: The server's state could not be found!");
			initDataStructures();
		}
		catch(IOException e){
			System.err.println("Warning: There was an error reading the server's state! "+e);
			initDataStructures();
		}
		catch(ClassNotFoundException e){
			System.err.println("Warning: There was an error reading the server's state!"+e);
			initDataStructures();
		}*/
		matchingsDB=Db4o.openFile(SERVER_STATE_DIR+"/matchings.db");
		categoriesDB=Db4o.openFile(SERVER_STATE_DIR+"/categories.db");
		accessCountDB=Db4o.openFile(SERVER_STATE_DIR+"/accesscount.db");
		mostRecentDB=Db4o.openFile(SERVER_STATE_DIR+"/mostrecent.db");
		mostPopularDB=Db4o.openFile(SERVER_STATE_DIR+"/mostpopular.db");
		userDB=Db4o.openFile(SERVER_STATE_DIR+"/users.db");
		
		List access = accessCountDB.query(new Predicate() {
			public boolean match(Map m){
				return true;
			}
		});
		if(access.size()>0)
			myAccessCount = (HashMap)access.get(0);
		else
			myAccessCount = new HashMap();
		
		List popular = mostPopularDB.query(new Predicate() {
			public boolean match(Set s){
				return true;
			}
		});
		if(popular.size()>0)
			myMostPopular = (HashSet)popular.get(0);
		else
			myMostPopular = new HashSet();
		
		List recent = mostRecentDB.query(new Predicate() {
			public boolean match(Map m){
				return true;
			}
		});
		if(recent.size()>0)
			myMostRecent = (TreeMap)recent.get(0);
		else
			myMostRecent = new TreeMap();
		
		myLoggedInUsers = new HashMap();
		myHead2HeadGames = new HashMap();
		waitingPlayerNum = -1;
		myRecentGame = null;
		
		ServletContext con = this.getServletContext();
		con.setAttribute(COOKIE_ATTRIBUTE, myLoggedInUsers);
	}
	
	//This will close all databases in order to dump their meta-data and then reinitialize the databases
	private void makePersistent(){
		/*try{
			FileOutputStream fos = new FileOutputStream(SERVER_STATE_FILE);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(myMatchings);
			oos.writeObject(myCategories);
			oos.writeObject(myAccessCount);
			oos.writeObject(myMostPopular);
			oos.writeObject(myMostRecent);
			oos.close();
		}
		catch(FileNotFoundException e){
			System.err.println("Warning: This server is not being backed up!");
		}
		catch(IOException e){
			System.err.println("Warning: Error occurred while saving state!");
		}*/
		matchingsDB.close();
		categoriesDB.close();
		accessCountDB.close();
		mostRecentDB.close();
		mostPopularDB.close();
		userDB.close();
		initServerState();
	}
	
	//This will initialize the servlet's data structures if necessary
	/*private void initDataStructures(){
		myMatchings = new HashMap();
		myCategories = new HashMap();
		myAccessCount = new HashMap();
		myMostPopular = new HashSet();
		myMostRecent = new TreeMap();
	}*/
}